/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2012-2016 ForgeRock AS.
 */
package org.opends.quicksetup.util;

import java.io.*;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.quicksetup.*;

import static org.opends.messages.QuickSetupMessages.*;
import static com.forgerock.opendj.util.OperatingSystem.isUnix;

/**
 * Utility class for use by applications containing methods for managing
 * file system files.  This class handles application notifications for
 * interesting events.
 */
public class FileManager {
  /** Describes the approach taken to deleting a file or directory. */
  public enum DeletionPolicy {
    /** Delete the file or directory immediately. */
    DELETE_IMMEDIATELY,
    /** Mark the file or directory for deletion after the JVM has exited. */
    DELETE_ON_EXIT,
    /**
     * First try to delete the file immediately.  If the deletion was
     * unsuccessful mark the file for deletion when the JVM has existed.
     */
    DELETE_ON_EXIT_IF_UNSUCCESSFUL
  }

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private Application application;

  /** Creates a new file manager. */
  public FileManager() {
    // do nothing;
  }

  /**
   * Creates a new file manager.
   * @param app Application managing files to which progress notifications
   * will be sent
   */
  public FileManager(Application app) {
    this.application = app;
  }

  /**
   * Recursively copies any files or directories appearing in
   * <code>source</code> or a subdirectory of <code>source</code>
   * to the corresponding directory under <code>target</code>.  Files
   * in under <code>source</code> are not copied to <code>target</code>
   * if a file by the same name already exists in <code>target</code>.
   *
   * @param source source directory
   * @param target target directory
   * @throws ApplicationException if there is a problem copying files
   */
  public void synchronize(File source, File target)
          throws ApplicationException
  {
    if (source != null && target != null) {
      String[] sourceFileNames = source.list();
      if (sourceFileNames != null) {
        for (String sourceFileName : sourceFileNames) {
          File sourceFile = new File(source, sourceFileName);
          copyRecursively(sourceFile, target, null, false);
        }
      }
    }
  }

  /**
   * Renames the source file to the target file.  If the target file exists
   * it is first deleted.  The rename and delete operation return values
   * are checked for success and if unsuccessful, this method throws an exception.
   *
   * @param fileToRename The file to rename.
   * @param target       The file to which <code>fileToRename</code> will be moved.
   * @throws ApplicationException If a problem occurs while attempting to rename
   *                     the file.  On the Windows platform, this typically
   *                     indicates that the file is in use by this or another
   *                     application.
   */
  public void rename(File fileToRename, File target)
          throws ApplicationException {
    if (fileToRename != null && target != null) {
      synchronized (target) {
        if (target.exists() && !target.delete())
        {
          throw new ApplicationException(
                  ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
                  INFO_ERROR_DELETING_FILE.get(Utils.getPath(target)), null);
        }
      }
      if (!fileToRename.renameTo(target)) {
        throw new ApplicationException(
                ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
                INFO_ERROR_RENAMING_FILE.get(Utils.getPath(fileToRename),
                        Utils.getPath(target)), null);
      }
    }
  }

  /**
   * Move a file.
   * @param object File to move
   * @param newParent File representing new parent directory
   * @throws ApplicationException if something goes wrong
   */
  public void move(File object, File newParent) throws ApplicationException
  {
    move(object, newParent, null);
  }

  /**
   * Move a file.
   * @param object File to move
   * @param newParent File representing new parent directory
   * @param filter that will be asked whether the operation should be performed
   * @throws ApplicationException if something goes wrong
   */
  public void move(File object, File newParent, FileFilter filter)
          throws ApplicationException
  {
    // TODO: application notification
    if (filter == null || filter.accept(object)) {
      new MoveOperation(object, newParent).apply();
    }
  }

  /**
   * Deletes a single file or directory.
   * @param object File to delete
   * @throws ApplicationException if something goes wrong
   */
  public void delete(File object)
          throws ApplicationException
  {
    delete(object, null);
  }

  /**
   * Deletes a single file or directory.
   * @param object File to delete
   * @param filter that will be asked whether the operation should be performed
   * @throws ApplicationException if something goes wrong
   */
  public void delete(File object, FileFilter filter)
          throws ApplicationException
  {
    if (filter == null || filter.accept(object)) {
      new DeleteOperation(object, DeletionPolicy.DELETE_IMMEDIATELY).apply();
    }
  }

  /**
   * Deletes the children of a directory.
   *
   * @param parentDir the directory whose children is deleted
   * @throws ApplicationException if there is a problem deleting children
   */
  public void deleteChildren(File parentDir) throws ApplicationException {
    if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) {
      File[] children = parentDir.listFiles();
      if (children != null) {
        for (File child : children) {
          deleteRecursively(child);
        }
      }
    }
  }

  /**
   * Deletes everything below the specified file.
   *
   * @param file the path to be deleted.
   * @throws org.opends.quicksetup.ApplicationException if something goes wrong.
   */
  public void deleteRecursively(File file) throws ApplicationException {
    deleteRecursively(file, null,
            FileManager.DeletionPolicy.DELETE_IMMEDIATELY);
  }

  /**
   * Deletes everything below the specified file.
   *
   * @param file   the path to be deleted.
   * @param filter the filter of the files to know if the file can be deleted
   *               directly or not.
   * @param deletePolicy describes how deletions are to be made
   *        JVM exits rather than deleting the files immediately.
   * @throws ApplicationException if something goes wrong.
   */
  public void deleteRecursively(File file, FileFilter filter,
                                DeletionPolicy deletePolicy)
          throws ApplicationException {
    operateRecursively(new DeleteOperation(file, deletePolicy), filter);
  }

  /**
   * Copies everything below the specified file.
   *
   * @param objectFile   the file to be copied.
   * @param destDir      the directory to copy the file to
   * @return File representing the destination
   * @throws ApplicationException if something goes wrong.
   */
  public File copy(File objectFile, File destDir)
          throws ApplicationException
  {
    CopyOperation co = new CopyOperation(objectFile, destDir, false);
    co.apply();
    return co.getDestination();
  }

  /**
   * Copies everything below the specified file.
   *
   * @param objectFile   the file to be copied.
   * @param destDir      the directory to copy the file to
   * @param overwrite    overwrite destination files.
   * @return File representing the destination
   * @throws ApplicationException if something goes wrong.
   */
  public File copy(File objectFile, File destDir, boolean overwrite)
          throws ApplicationException
  {
    CopyOperation co = new CopyOperation(objectFile, destDir, overwrite);
    co.apply();
    return co.getDestination();
  }

  /**
   * Copies everything below the specified file.
   *
   * @param objectFile   the file to be copied.
   * @param destDir      the directory to copy the file to
   * @throws ApplicationException if something goes wrong.
   */
  public void copyRecursively(File objectFile, File destDir)
          throws ApplicationException
  {
    copyRecursively(objectFile, destDir, null);
  }

  /**
   * Copies everything below the specified file.
   *
   * @param objectFile   the file to be copied.
   * @param destDir      the directory to copy the file to
   * @param filter the filter of the files to know if the file can be copied
   *               directly or not.
   * @throws ApplicationException if something goes wrong.
   */
  public void copyRecursively(File objectFile, File destDir, FileFilter filter)
          throws ApplicationException {
    copyRecursively(objectFile, destDir, filter, false);
  }

  /**
   * Copies everything below the specified file.
   *
   * @param objectFile   the file to be copied.
   * @param destDir      the directory to copy the file to
   * @param filter the filter of the files to know if the file can be copied
   *               directly or not.
   * @param overwrite    overwrite destination files.
   * @throws ApplicationException if something goes wrong.
   */
  public void copyRecursively(File objectFile, File destDir,
                              FileFilter filter, boolean overwrite)
          throws ApplicationException {
    operateRecursively(new CopyOperation(objectFile, destDir, overwrite), filter);
  }

 /**
  * Determines whether two files differ in content.
  *
  * @param f1 file to compare
  * @param f2 file to compare
  * @return boolean where true indicates that two files differ
  * @throws IOException if there is a problem reading the files' contents
  */
 public boolean filesDiffer(File f1, File f2) throws IOException {
   boolean differ = false;
    try (FileReader fr1 = new FileReader(f1);
        FileReader fr2 = new FileReader(f2))
    {
     boolean done = false;
     while (!differ && !done) {
       int c1 = fr1.read();
       int c2 = fr2.read();
       differ = c1 != c2;
       done = c1 == -1 || c2 == -1;
     }
   }
   return differ;
 }

  private void operateRecursively(FileOperation op, FileFilter filter)
          throws ApplicationException {
    File file = op.getObjectFile();
    if (file.exists()) {
      if (file.isFile()) {
        if (filter != null) {
          if (filter.accept(file)) {
            op.apply();
          }
        } else {
          op.apply();
        }
      } else {
        File[] children = file.listFiles();
        if (children != null) {
          for (File aChildren : children) {
            FileOperation newOp = op.copyForChild(aChildren);
            operateRecursively(newOp, filter);
          }
        }
        if (filter != null) {
          if (filter.accept(file)) {
            op.apply();
          }
        } else {
          op.apply();
        }
      }
    } else {
      // Just tell that the file/directory does not exist.
      if (application != null) {
        application.notifyListeners(application.getFormattedWarning(
                INFO_FILE_DOES_NOT_EXIST.get(file)));
      }
      logger.info(LocalizableMessage.raw("file '" + file + "' does not exist"));
    }
  }

  /** A file operation. */
  private abstract class FileOperation {
    private File objectFile;

    /**
     * Creates a new file operation.
     * @param objectFile to be operated on
     */
    public FileOperation(File objectFile) {
      this.objectFile = objectFile;
    }

    /**
     * Gets the file to be operated on.
     * @return File to be operated on
     */
    protected File getObjectFile() {
      return objectFile;
    }

    /**
     * Make a copy of this class for the child file.
     * @param child to act as the new file object
     * @return FileOperation as the same type as this class
     */
    public abstract FileOperation copyForChild(File child);

    /**
     * Execute this operation.
     * @throws ApplicationException if there is a problem.
     */
    public abstract void apply() throws ApplicationException;
  }

  /** A copy operation. */
  private class CopyOperation extends FileOperation {
    private File destination;

    private boolean overwrite;

    /**
     * Create a new copy operation.
     * @param objectFile to copy
     * @param destDir to copy to
     * @param overwrite if true copy should overwrite any existing file
     */
    public CopyOperation(File objectFile, File destDir, boolean overwrite) {
      super(objectFile);
      this.destination = new File(destDir, objectFile.getName());
      this.overwrite = overwrite;
    }

    @Override
    public FileOperation copyForChild(File child) {
      return new CopyOperation(child, destination, overwrite);
    }

    /**
     * Returns the destination file that is the result of copying
     * <code>objectFile</code> to <code>destDir</code>.
     * @return The destination file.
     */
    public File getDestination() {
      return this.destination;
    }

    @Override
    public void apply() throws ApplicationException {
      File objectFile = getObjectFile();
      if (objectFile.isDirectory()) {
        if (!destination.exists()) {
          destination.mkdirs();
        }
      } else {
        // If overwriting and the destination exists then kill it
        if (destination.exists() && overwrite) {
          deleteRecursively(destination);
        }

        if (!destination.exists()) {
          if (Utils.ensureParentsExist(destination)) {
            if (application != null && application.isVerbose()) {
              application.notifyListeners(application.getFormattedWithPoints(
                      INFO_PROGRESS_COPYING_FILE.get(
                              objectFile.getAbsolutePath(),
                              destination.getAbsolutePath())));
            }
            logger.info(LocalizableMessage.raw("copying file '" +
                    objectFile.getAbsolutePath() + "' to '" +
                    destination.getAbsolutePath() + "'"));
            try (FileInputStream fis = new FileInputStream(objectFile);
                FileOutputStream fos = new FileOutputStream(destination))
            {
              byte[] buf = new byte[1024];
              int i;
              while ((i = fis.read(buf)) != -1) {
                fos.write(buf, 0, i);
              }
              if (destination.exists() && isUnix()) {
                // TODO:  set the file's permissions.  This is made easier in
                // Java 1.6 but until then use the TestUtilities methods
                String permissions = Utils.getFileSystemPermissions(objectFile);
                Utils.setPermissionsUnix(Utils.getPath(destination), permissions);
              }

              if (application != null && application.isVerbose()) {
                application.notifyListeners(
                        application.getFormattedDoneWithLineBreak());
              }
            } catch (Exception e) {
              LocalizableMessage errMsg = INFO_ERROR_COPYING_FILE.get(
                      objectFile.getAbsolutePath(),
                      destination.getAbsolutePath());
              throw new ApplicationException(
                      ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
                      errMsg, null);
            }
          } else {
            LocalizableMessage errMsg = INFO_ERROR_COPYING_FILE.get(
                    objectFile.getAbsolutePath(),
                    destination.getAbsolutePath());
            throw new ApplicationException(
                    ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
                    errMsg, null);
          }
        } else {
          logger.info(LocalizableMessage.raw("Ignoring file '" +
                  objectFile.getAbsolutePath() + "' since '" +
                  destination.getAbsolutePath() + "' already exists"));
          if (application != null && application.isVerbose()) {
            application.notifyListeners(
                    INFO_INFO_IGNORING_FILE.get(
                                    objectFile.getAbsolutePath(),
                                    destination.getAbsolutePath()));
            application.notifyListeners(application.getLineBreak());
          }
        }
      }
    }
  }

  /** A delete operation. */
  private class DeleteOperation extends FileOperation {
    private DeletionPolicy deletionPolicy;

    /**
     * Creates a delete operation.
     * @param objectFile to delete
     * @param deletionPolicy describing how files will be deleted
     * is to take place after this program exists.  This is useful
     * for cleaning up files that are currently in use.
     */
    public DeleteOperation(File objectFile, DeletionPolicy deletionPolicy) {
      super(objectFile);
      this.deletionPolicy = deletionPolicy;
    }

    @Override
    public FileOperation copyForChild(File child) {
      return new DeleteOperation(child, deletionPolicy);
    }

    @Override
    public void apply() throws ApplicationException {
      File file = getObjectFile();
      boolean isFile = file.isFile();

      if (application != null && application.isVerbose()) {
        if (isFile) {
          application.notifyListeners(application.getFormattedWithPoints(
                  INFO_PROGRESS_DELETING_FILE.get(file.getAbsolutePath())));
        } else {
          application.notifyListeners(application.getFormattedWithPoints(
                  INFO_PROGRESS_DELETING_DIRECTORY.get(
                          file.getAbsolutePath())));
        }
      }
      logger.info(LocalizableMessage.raw("deleting " +
              (isFile ? " file " : " directory ") +
              file.getAbsolutePath()));

      boolean delete = false;
      /*
       * Sometimes the server keeps some locks on the files.
       * TODO: remove this code once stop-ds returns properly when server
       * is stopped.
       */
      int nTries = 5;
      for (int i = 0; i < nTries && !delete; i++) {
        if (DeletionPolicy.DELETE_ON_EXIT.equals(deletionPolicy)) {
          file.deleteOnExit();
          delete = true;
        } else {
          delete = file.delete();
          if (!delete && DeletionPolicy.DELETE_ON_EXIT_IF_UNSUCCESSFUL.
                  equals(deletionPolicy)) {
            file.deleteOnExit();
            delete = true;
          }
        }
        if (!delete) {
          try {
            Thread.sleep(1000);
          }
          catch (Exception ex) {
            // do nothing;
          }
        }
      }

      if (!delete) {
        LocalizableMessage errMsg;
        if (isFile) {
          errMsg = INFO_ERROR_DELETING_FILE.get(file.getAbsolutePath());
        } else {
          errMsg = INFO_ERROR_DELETING_DIRECTORY.get(file.getAbsolutePath());
        }
        throw new ApplicationException(
                ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
                errMsg, null);
      }

      if (application != null && application.isVerbose()) {
        application.notifyListeners(
                application.getFormattedDoneWithLineBreak());
      }
    }
  }

  /** A delete operation. */
  private class MoveOperation extends FileOperation {
    File destination;

    /**
     * Creates a delete operation.
     * @param objectFile to delete
     * @param newParent File where <code>objectFile</code> will be copied.
     */
    public MoveOperation(File objectFile, File newParent) {
      super(objectFile);
      this.destination = new File(newParent, objectFile.getName());
    }

    @Override
    public FileOperation copyForChild(File child) {
      return new MoveOperation(child, destination);
    }

    @Override
    public void apply() throws ApplicationException {
      File objectFile = getObjectFile();
      if (destination.exists()) {
        deleteRecursively(destination);
      }
      if (!objectFile.renameTo(destination)) {
        throw ApplicationException.createFileSystemException(
                INFO_ERROR_FAILED_MOVING_FILE.get(Utils.getPath(objectFile),
                        Utils.getPath(destination)),
                null);
      }
    }
  }
}
